mapメソッドの話
mapメソッドの話
今回は、普段よくつかう map
メソッドについてお話します。
本記事は「ポエム特集」のカテゴリの記事であり、原典とする書籍等が存在するものではありません。予めご承知おきください。
mapのoverview
List#map
は普段よく使います。
任意の関数を引数にとり、各要素にそれぞれ適用した結果のリストを返します。
scala> List(1, 2, 3).map(_ + 10) res1: List[Int] = List(11, 12, 13)
Option#map
は、その次によく使います(よね?)。
こちらは自身が Some(x)
で引数の関数が f
のとき、 Some(f(x))
を返します。
また、自身が None
であれば直ちに None
を返します。以前これを「文脈を保つ特徴」と説明しました。
scala> Some(1).map(_ + 10) res2: Option[Int] = Some(11) scala> Option.empty[Int].map(_ + 10) res3: Option[Int] = None
このような特徴は map
メソッドを持つ他の型についても言えます。
scala> List.empty[Int].map(_ + 10) res4: List[Int] = List()
#import文は省略 scala> val fa = Future.failed[Int](new RuntimeException).map(_ + 10) fa: scala.concurrent.Future[Int] = Failure(java.lang.RuntimeException) scala> Await.result(fa, Duration.Inf) java.lang.RuntimeException ... 32 elided
mapを作る
いままで紹介した map
は全て、もともと存在するメソッドでした。
final case class Wrap[+A](get: A) // example val wa = Wrap(123) wa.get // => 123 val wb = Wrap("xxx") wb.get // => "xxx"
いま、私たちは簡素な型 Wrap
を作りました。この新しい型に map
メソッドを与えてみましょう。
final case class Wrap[+A](get: A) { def map[B](f: A => B): Wrap[B] = Wrap(f(get)) } // example Wrap(123).map(x => s"The number is $x").get // => "The number is 123"
多くの方が、直感的に実装を書くことができると思います。
じゃあ次はこんな型を作って見ます。
sealed trait ColorWrap[+A] final case class BlueWrap[+A](get: A) extends ColorWrap[A] final case class RedWrap[+A](get: A) extends ColorWrap[A] final case class GreenWrap[+A](get: A) extends ColorWrap[A]
色のついた wrap で ColorWrap
です。 colored wrap にしたほうが英語っぽいですが細かいことは気にしません。
これも多くの方が直感的に map
を書けると思います。
sealed trait ColorWrap[+A] { def map[B](f: A => B): ColorWrap[B] = this match { case BlueWrap(v) => BlueWrap(f(v)) case RedWrap(v) => RedWrap(f(v)) case GreenWrap(v) => GreenWrap(f(v)) } } final case class BlueWrap[+A](get: A) extends ColorWrap[A] final case class RedWrap[+A](get: A) extends ColorWrap[A] final case class GreenWrap[+A](get: A) extends ColorWrap[A]
こんな風に。 f
を適用しつつ、青ならば青を、赤ならば赤を返すようなメソッドを書くと思います。
この「青なら map
しても青のまま」という特徴は、先ほど私たちが「文脈を保つ」と表現したものでしょう。私たちは(大抵のばあい)それを経験的に感じ取っています。
ところで。もし直感に抗っていいなら、青だろうが赤だろうが必ず緑を返す map
も作れますよね。
sealed trait ColorWrap[+A] { def map[B](f: A => B): ColorWrap[B] = this match { case BlueWrap(v) => GreenWrap(f(v)) case RedWrap(v) => GreenWrap(f(v)) case GreenWrap(v) => GreenWrap(f(v)) } }
文脈は保たれなくなりましたが、コンパイル通るんだから何も問題ないですね?
私たちの map
が緑ばかり返すことを、いったい誰が咎めると言うのだろうか。
mapの決まりごと
結論から言ってしまいます。 map
メソッドを作る際には「守らなければならない決まり」があります。
hoge
と 「hoge.map(…)
に恒等関数(引数をそのまま返す関数)をつっこんだもの」 が等しいhoge.map(f).map(g)
とhoge.map(x => g(f(x)))
が等しい
この決まりを守ってない map
は嘘つきです。さぞ皆から咎められるでしょう。
今まで、「文脈を保つ」という直感的な表現をしていた map
の特徴は、主に 1. の決まりによって遵守されます。
さきほどの、緑ばかり返す ColorWrap#map
を思い出してください。
// 緑ばかり返す map を使う例 // Note: x => x は identity とも書けます // 1. val blue = BlueWrap(123) blue == blue.map(x => x) // => false; blue.map(x => x) は GreenMap(123) を返します // 2. val f: Int => Int = _ + 1 val g: Int => String = "v=" + _ blue.map(f).map(g) // => GreenWrap(v=124) blue.map(x => g(f(x))) // => GreenWrap(v=124)
blue
と blue.map
に恒等関数をつっこんだものが等しくないです。これでは嘘つきの map
です。
(2. は守られていますが、 1. は守られていません)
一方、ColorWrap
に最初に与えた、直感的な map
メソッドはこの規則を守っています。
// それぞれの色を返す map を使う例 // Note: x => x は identity とも書けます val blue = BlueWrap(123) blue == blue.map(x => x) // => true val red = RedWrap("aaa") red == red.map(x => x) // => true
もちろん 2. も守られています *1。ぜひ試してみてください。
まとめ
map
メソッドが(直感的に)「文脈を保つ」ことは、map
メソッドの決まりごとによって保証されています。map
メソッドを自分で作るときは、この法則を守るようにしてください。みんな幸せになります。
今回は、ふだん何気なく使っている map
メソッドについて、そのさわりを書いてみました。
「もっと具体的に知りたい」と思った方は、以下の参考リンクから旅をしてみてください。それはきっと長く、そして楽しい旅になると思います。
ではまた!